<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="coinfaces">

        <title>Linux僵尸进程</title>

    <link rel="stylesheet" href="../theme/css/style.css">
    <link rel="stylesheet" href="../theme/css/pygments.css">



</head>

<body>
    <div class="content">
<h1>Linux僵尸进程</h1>    <p>
        under
    </p>
    <p>
        in <a href="../categories/draft/">draft</a>
    </p>
    <p>Published: 2020-02-27</p>


    <p>在Linux中，可能是为了让父进程全面掌握子进程的信息，进程退出后，系统进程表中仍保留其信息，直到另一个进程来料理它的后事（执行<code>wait</code>系统调用）。处于这种状态的进程，<code>ps</code>命令可以查询到其PID，但杀不死（即使用<code>kill -9 $PID</code>），因此被称为僵尸进程（zombie process）。</p>
<h2>产生</h2>
<p>编译运行下面的程序就能产生一个僵尸进程：</p>
<div class="highlight"><pre><span></span><code><span class="cp">#include</span><span class="cpf">&lt;stdio.h&gt;</span><span class="cp"></span>
<span class="cp">#include</span><span class="cpf">&lt;unistd.h&gt;</span><span class="cp"></span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">int</span> <span class="n">pid</span> <span class="o">=</span> <span class="n">fork</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">pid</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// 该子进程将退出</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="c1">// 父进程持续运行，但不对子进程执行wait系统调用</span>
        <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>


<p>运行<code>ps -ef | grep defunct</code>可以看到这个僵尸进程：</p>
<div class="highlight"><pre><span></span><code><span class="err">root      5965  5964  0 18:42 pts/7    00:00:00 [a.out] &lt;defunct&gt;</span>
</code></pre></div>


<h2>问题</h2>
<p>僵尸进程本身不是问题，但长期存在可能引起问题：</p>
<ul>
<li>
<p>Shell脚本常用<code>kill -s 0 $PID</code>来判断进程的存在，但这个命令不能区分普通进程和僵尸进程。如果单以这个标准来判断某服务是否需要重启，便会误入歧途。</p>
</li>
<li>
<p>如果僵尸进程不断产生，将耗尽系统进程表，导致不能创建新进程。在终端（随时可用<code>Ctrl+C</code>终止其运行）运行下面的程序模拟这种情况。</p>
<p>```cpp</p>
<h1>include<stdio.h></h1>
<h1>include<stdlib.h></h1>
<h1>include<unistd.h></h1>
<p>int main()
{
    int child_serial = 0;
    int print_serial = 1;
    int last_failed = 0;
    int pid;</p>
<div class="highlight"><pre><span></span><code><span class="err">while (1) {</span>
<span class="err">    pid = fork();</span>
<span class="err">    if (pid &lt; 0) {</span>
<span class="err">        // 父进程，fork失败</span>
<span class="err">        if (child_serial &gt; last_failed) {</span>
<span class="err">            last_failed = child_serial;</span>
<span class="err">            printf(&quot;Fork failed, child number = %d\n&quot;, child_serial);</span>
<span class="err">            printf(&quot;Parent continue running\n&quot;);</span>
<span class="err">        }</span>
<span class="err">    } else if (pid &gt; 0) {</span>
<span class="err">        // 父进程，fork成功</span>
<span class="err">        child_serial += 1;</span>
<span class="err">        if (child_serial == print_serial) {</span>
<span class="err">            printf(&quot;Forked child %d\n&quot;, child_serial);</span>
<span class="err">            print_serial &lt;&lt;= 1;</span>
<span class="err">        }</span>
<span class="err">    } else {</span>
<span class="err">        // 子进程，退出后成为僵尸进程</span>
<span class="err">        exit(0);</span>
<span class="err">    }</span>
<span class="err">}</span>
</code></pre></div>


<p>}
```</p>
<p>这个程序运行后，如果想要执行其它命令（包括关机和重启），将遇到下面的错误提示：</p>
<p><code>fork: retry: Resource temporarily unavailable</code></p>
</li>
</ul>
<h2>Docker</h2>
<p>僵尸进程的存在依赖两个条件：</p>
<ul>
<li>父进程没有正确处理子进程的退出</li>
<li>父进程在子进程退出后继续运行</li>
</ul>
<p>如果父进程先于子进程退出运行，就不必处理子进程的退出。这是因为一个进程停止运行后，其所有子进程都会称为init进程（PID 1）的子进程，而普通Linux系统的init进程会在子进程退出时对其（以及对已退出的子进程）执行<code>wait</code>系统调用，这个动作叫做reap（也有例外情况：其它进程也可以通过<code>prctl</code>系统调用申请成为次级reaper，负责接管其后代进程的reap工作）。</p>
<p>在docker容器里，init进程（PID 1）的程序是用户指定的，这个程序可能不具备reap功能，这意味着在docker容器里僵尸进程数量更容易累积。下面的这个Python程序，在宿主系统里运行时不会产生僵尸进程：</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">subprocess</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">time</span>


<span class="k">def</span> <span class="nf">writeline</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
    <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">s</span> <span class="o">+</span> <span class="s1">&#39;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
    <span class="n">sys</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>


<span class="k">def</span> <span class="nf">daemon_worker</span><span class="p">():</span>
    <span class="n">writeline</span><span class="p">(</span><span class="s1">&#39;Run scheduled task&#39;</span><span class="p">)</span>
    <span class="n">pid1</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">fork</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">pid1</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="c1"># child</span>
        <span class="n">writeline</span><span class="p">(</span><span class="s1">&#39;Prepare environment for worker&#39;</span><span class="p">)</span>
        <span class="n">pid2</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">fork</span><span class="p">()</span>
        <span class="k">if</span> <span class="n">pid2</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
            <span class="c1"># grand child</span>
            <span class="n">writeline</span><span class="p">(</span><span class="s1">&#39;Spawn daemon worker </span><span class="si">{}</span><span class="s1"> from </span><span class="si">{}</span><span class="s1">&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getpid</span><span class="p">(),</span> <span class="n">os</span><span class="o">.</span><span class="n">getppid</span><span class="p">()))</span>
            <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
            <span class="n">writeline</span><span class="p">(</span><span class="s1">&#39;Work complete&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getpid</span><span class="p">()))</span>
            <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">()</span>
            <span class="c1"># PID 1 will handle its exit</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">()</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="c1"># prevent child from becoming zombie</span>
        <span class="n">os</span><span class="o">.</span><span class="n">wait</span><span class="p">()</span>


<span class="k">def</span> <span class="nf">scheduler</span><span class="p">():</span>
    <span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
        <span class="n">daemon_worker</span><span class="p">()</span>
        <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>


<span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span>
    <span class="n">scheduler</span><span class="p">()</span>
</code></pre></div>


<p>但作为docker容器的init进程就会有问题：</p>
<div class="highlight"><pre><span></span><code><span class="k">FROM</span> <span class="s">centos:7</span>

<span class="k">ADD</span> z.py /z.py
<span class="k">CMD</span> python z.py
</code></pre></div>


<p>运行上面的镜像，就可以看到系统中出现越来越多的僵尸进程。</p>
<p>如果不加限制，docker容器里的僵尸进程也会耗尽宿主系统中的进程表资源，导致宿主机不能创建进程，可在启动容器时指定<code>--pids-limit</code>参数来防范这种情况的发生：</p>
<div class="highlight"><pre><span></span><code>docker container run --pids-limit <span class="m">100</span> <span class="nv">$your_image</span>
</code></pre></div>
    </div>
<nav class="nav sidebar">
    <div class="siteinfo">
        <div class="sitename"><a href="..">coinfaces</a></div>
        <div class="tagline">Another Side of Life</div>
    </div>

        <a href="../categories/">categories</a>
        <a href="..">&larr; Home</a>
</nav><footer class="footer">
    <span>
        &copy; jpli &middot;
        under <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA 4.0</a>
    </span>
    |
    <span>
        Powered by:
        <a href="http://blog.getpelican.com/">Pelican</a>
    </span>
</footer></body>
</html>